#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import sys
import time
import json
import shutil
import zipfile
import logging
import platform
import argparse
import subprocess


from main_window_widget import Ui_MainWindow
from PySide6.QtWidgets import QApplication, QWidget, QMessageBox
from PySide6.QtCore import QThread, Signal, SignalInstance, Slot, QTimer


MSG_UNKNOWN_ERROR = 'Unknown error: [{:s}]'
ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
WINDOW_TITLE = 'Custom Backup Script'


def read_json_file(file_path, logger_name):
    logger = logging.getLogger(logger_name)
    try:
        with open(file_path, 'r', encoding='utf-8') as fd:
            json_str = fd.read()
        json_obj = json.loads(json_str)
    except Exception as e:
        logger.error(MSG_UNKNOWN_ERROR.format(str(e)))
        return None
    return json_obj


def setup_logger(logger_file, logger_name):
    fmt = '< %(name)s - %(levelname)s - %(filename)s - %(lineno)d - %(asctime)s >: %(message)s'
    fmt_obj = logging.Formatter(fmt)

    fh = logging.FileHandler(logger_file)
    fh.setFormatter(fmt_obj)
    fh.setLevel(logging.DEBUG)

    sh = logging.StreamHandler()
    sh.setFormatter(fmt_obj)
    sh.setLevel(logging.INFO)

    logger = logging.getLogger(logger_name)
    assert logger is not None
    logger.setLevel(logging.DEBUG)
    logger.addHandler(fh)
    logger.addHandler(sh)
    logger.info('Logger [{:s}] start running! Log file: [{:s}]'.format(logger_name, os.path.realpath(logger_file)))


LABEL_TASK_CNT_TEXT_TO_FORMAT = 'Task: {0:d}/{1:d}'


TS = time.time_ns()
LOGGER_FILE = os.path.realpath(os.path.join(ROOT_DIR, 'logs/log{:d}.txt'.format(TS)))
LOGGER_NAME = 'logger0'
setup_logger(LOGGER_FILE, LOGGER_NAME)
LOGGER = logging.getLogger(LOGGER_NAME)
assert LOGGER is not None

TEMPLATE_BACKUP_LIST_FILE = os.path.realpath(os.path.join(ROOT_DIR, 'template_backup_list.json'))
BACKUP_LIST_FILE = os.path.realpath(os.path.join(ROOT_DIR, 'backup_list.json'))
if not os.path.isfile(BACKUP_LIST_FILE):
    shutil.copy(TEMPLATE_BACKUP_LIST_FILE, BACKUP_LIST_FILE)
BACKUP_LIST_OBJ = read_json_file(BACKUP_LIST_FILE, LOGGER_NAME)
assert BACKUP_LIST_OBJ is not None
OS_NAME = platform.system()


LOGGER.info('\n>>>>>>>>\nConfigurations:\nRoot dir: {:s}\nBackup list file: {:s}\nOS: {:s}\n<<<<<<<<'
            .format(ROOT_DIR, os.path.realpath(BACKUP_LIST_FILE), OS_NAME))


# 主窗口类
class MainWindow(QWidget):
    signal_cfg_change: SignalInstance = Signal()
    signal_start_backup: SignalInstance = Signal()
    signal_kill_backup_thread: SignalInstance = Signal()

    def __init__(self, is_auto=False, title='NO TITLE', parent=None):
        super().__init__(parent)

        self.logger = logging.getLogger(LOGGER_NAME)
        self.logger.debug('MainWindow initialization START!')

        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.setFixedSize(self.size())
        self.setWindowTitle(title)

        self.success_list = []
        self.is_auto = is_auto

        self.backup_thread = BackupThread(self)
        self.backup_thread.start()

        self.signal_cfg_change.connect(self.slot_cfg_change)
        self.ui.button_edit_backup_list.clicked.connect(self.slot_button_edit_backup_list_clicked)
        self.ui.button_start_to_backup.clicked.connect(self.slot_button_start_to_backup_clicked)
        self.signal_kill_backup_thread.connect(self.backup_thread.slot_kill)
        self.signal_start_backup.connect(self.backup_thread.slot_start_backup)
        self.signal_start_backup.connect(self.slot_start_backup)

        self.n_tasks = -1
        self.n_complete_tasks = 0
        self.ui.progbar_backup_progress.setMaximum(100)
        self.ui.progbar_backup_progress.setMinimum(0)
        self.ui.progbar_backup_progress.setValue(0)

        self.signal_cfg_change.emit()
        self.logger.debug('MainWindow initialization COMPLETE!')

        if is_auto:
            self.logger.info('Auto backup!')
            self.signal_start_backup.emit()

    @Slot()
    def slot_start_backup(self):
        self.logger.debug('MainWindow slot on_start_backup executing...')
        self.ui.button_start_to_backup.setEnabled(False)
        self.ui.button_edit_backup_list.setEnabled(False)
        self.logger.debug('MainWindow slot on_start_backup execution done!')

    @Slot()
    def slot_cfg_change(self):
        global BACKUP_LIST_OBJ
        self.logger.debug('Slot MainWindow.on_cfg_change START')
        BACKUP_LIST_OBJ = read_json_file(BACKUP_LIST_FILE, self.logger.name)
        self.n_tasks = len(BACKUP_LIST_OBJ['list'])
        task_cnt_str = LABEL_TASK_CNT_TEXT_TO_FORMAT.format(0, self.n_tasks)
        self.ui.label_task_cnt.setText(task_cnt_str)
        self.ui.progbar_backup_progress.setMaximum(100)
        self.ui.progbar_backup_progress.setValue(0)
        self.logger.debug('Slot MainWindow.on_cfg_change DONE')

    @Slot(bool, int, int)
    def slot_task_end(self, success, task_idx, num_tasks):
        self.logger.debug('Slot MainWindow.on_task_end START, params: [success: {:s}, task_idx: {:d}, num_tasks: {:d}]'
                          .format(str(success), task_idx, num_tasks))
        self.logger.info('Task [{:d}/{:d}] '.format(task_idx+1, num_tasks) + 'success!' if success else 'fail!')
        self.success_list.append(success)
        self.n_complete_tasks = task_idx + 1
        task_cnt_str = LABEL_TASK_CNT_TEXT_TO_FORMAT.format(self.n_complete_tasks, self.n_tasks)
        self.ui.label_task_cnt.setText(task_cnt_str)
        self.ui.progbar_backup_progress.setValue((task_idx+1)/num_tasks*100)
        if task_idx == num_tasks - 1:
            n_success = 0
            for _ in self.success_list:
                n_success += 1
            msg = 'All tasks end, total tasks: {:d}, successful tasks: {:d}'.format(num_tasks, n_success)
            msgbox = QMessageBox(parent=self)
            msgbox.setWindowTitle('Task report')
            msgbox.setIcon(QMessageBox.Icon.Information)
            msgbox.setText(msg)
            if self.is_auto:
                QTimer.singleShot(5000, msgbox.accept)
            msgbox.exec()
            self.logger.info(msg)
            self.ui.button_start_to_backup.setEnabled(True)
            self.ui.button_edit_backup_list.setEnabled(True)
            if self.is_auto:
                self.logger.debug('Auto close')
                self.close()
        self.logger.debug('Slot MainWindow.on_task_end DONE')

    @Slot()
    def slot_button_edit_backup_list_clicked(self):
        self.logger.debug('User clicked button_edit_backup_list')
        assert OS_NAME == 'Windows' or OS_NAME == 'Linux' or OS_NAME == 'Darwin'
        if OS_NAME == 'Windows':
            os.startfile(BACKUP_LIST_FILE)
        elif OS_NAME == 'Linux':
            subprocess.call(['xdg_open', BACKUP_LIST_FILE])
        else:
            subprocess.call(['open', BACKUP_LIST_FILE])
        self.signal_cfg_change.emit()

    @Slot()
    def slot_button_start_to_backup_clicked(self):
        self.logger.debug('User clicked button_start_to_backup')
        self.signal_start_backup.emit()

    def closeEvent(self, event):
        self.logger.debug('Executing closeEvent()!')
        self.logger.debug('Emitting kill signal.')
        self.signal_kill_backup_thread.emit()
        self.logger.debug('Wait for child threads to finish')
        while not self.backup_thread.isFinished():
            time.sleep(0.05)
        self.logger.debug('closeEvent() done!')
        event.accept()


# 后台备份执行进程
class BackupThread(QThread):
    signal_task_end: SignalInstance = Signal(bool, int, int)

    def __init__(self, window_obj: MainWindow):
        super(BackupThread, self).__init__()
        self.logger = logging.getLogger(LOGGER_NAME)
        self.logger.debug('BackupThread initialization START!')
        self.busy = False
        self.to_kill = False
        self.signal_task_end.connect(window_obj.slot_task_end)
        self.logger.debug('BackupThread initialization END!')

    def run(self):
        while True:
            if self.to_kill:
                break
            else:
                if self.busy:
                    self.execute_backup_tasks(BACKUP_LIST_OBJ)
                    self.busy = False
                else:
                    self.msleep(100)
        self.exit(0)

    @Slot()
    def slot_start_backup(self):
        self.busy = True

    @Slot()
    def slot_kill(self):
        self.to_kill = True

    def execute_backup_tasks(self, backup_list_obj):
        self.logger.debug('BackupThread.execute_backup_tasks(...) START')
        mode = backup_list_obj['mode']
        backup_list = backup_list_obj['list']
        num_tasks = len(backup_list)
        for idx, task in enumerate(backup_list):
            src = task['source']
            dst = task['destination']
            task_type = task['type']

            if task_type == 'archive':
                if not os.path.isdir(os.path.dirname(dst)):
                    try:
                        os.makedirs(os.path.dirname(dst))
                    except OSError:
                        self.logger.error('OSError when creating dir: [{:s}]'.format(os.path.dirname(dst)))
                        self.signal_task_end.emit(False, idx, num_tasks)
                        continue
                    except Exception as e:
                        self.logger.error(MSG_UNKNOWN_ERROR.format(str(e)))
                        self.signal_task_end.emit(False, idx, num_tasks)
                        continue

                if os.path.isfile(src):
                    try:
                        with (zipfile.ZipFile(dst, 'w', zipfile.ZIP_STORED if mode == 'fast' else zipfile.ZIP_DEFLATED)
                              as zipf):
                            zipf.write(src, arcname=os.path.basename(src))
                    except Exception as e:
                        self.logger.error(MSG_UNKNOWN_ERROR.format(str(e)))
                        continue
                    self.signal_task_end.emit(True, idx, num_tasks)
                elif os.path.isdir(src):
                    src_parent_dir = os.path.dirname(src)
                    try:
                        with (zipfile.ZipFile(dst, 'w', zipfile.ZIP_STORED if mode == 'fast' else zipfile.ZIP_DEFLATED)
                              as zipf):
                            for dir_path, dir_names, filenames in os.walk(src):
                                target_dir = dir_path.replace(src_parent_dir, '')
                                for fn in filenames:
                                    src_file_path = str(os.path.join(dir_path, fn))
                                    dst_file_path = str(os.path.join(target_dir, fn))
                                    zipf.write(src_file_path, dst_file_path)
                    except Exception as e:
                        self.logger.error(MSG_UNKNOWN_ERROR.format(str(e)))
                        self.signal_task_end.emit(False, idx, num_tasks)
                        continue
                    self.signal_task_end.emit(True, idx, num_tasks)
                else:
                    self.logger.error('Source file or directory not found!')
                    self.signal_task_end.emit(False, idx, num_tasks)

            elif task_type == 'copy':
                if not os.path.isdir(os.path.dirname(dst)):
                    try:
                        os.makedirs(os.path.dirname(dst))
                    except OSError:
                        self.logger.error('OSError when creating dir: [{:s}]'.format(os.path.dirname(dst)))
                        self.signal_task_end.emit(False, idx, num_tasks)
                        continue
                    except Exception as e:
                        self.logger.error(MSG_UNKNOWN_ERROR.format(str(e)))
                        self.signal_task_end.emit(False, idx, num_tasks)
                        continue

                if os.path.isfile(src):
                    try:
                        shutil.copyfile(src, dst)
                    except Exception as e:
                        self.logger.error(MSG_UNKNOWN_ERROR.format(str(e)))
                        continue
                    self.signal_task_end.emit(True, idx, num_tasks)
                elif os.path.isdir(src):
                    try:
                        if os.path.isdir(dst):
                            shutil.rmtree(dst)
                        shutil.copytree(src, dst)
                    except Exception as e:
                        self.logger.error(MSG_UNKNOWN_ERROR.format(str(e)))
                        self.signal_task_end.emit(False, idx, num_tasks)
                        continue
                    self.signal_task_end.emit(True, idx, num_tasks)
                else:
                    self.logger.error('Source file or directory not found!')
                    self.signal_task_end.emit(False, idx, num_tasks)
            else:
                self.logger.error('Unknown type: [{:s}]'.format(task_type))
                self.signal_task_end.emit(False, idx, num_tasks)
        self.logger.debug('BackupThread.execute_backup_tasks(...) END')


# 主程序
if __name__ == "__main__":
    ps = argparse.ArgumentParser()
    ps.add_argument('--auto', default=False, action='store_true', help='Auto backup')
    args = ps.parse_args()
    app = QApplication(sys.argv)
    widget = MainWindow(is_auto=args.auto, title=WINDOW_TITLE)
    widget.show()
    sys.exit(app.exec())
